#!/usr/bin/env python
#
# Copyright 2008 Doug Hellmann.
#
"""VagueTextRecord format parser
"""

__version__ = "$Id$"

# Import system modules
import datetime
import logging
import re
import string
import time

# Import local modules


# Module

DEBUG = True
logger = logging.getLogger('docket.vtr')

def parse_stream(input, verbose=False, errorcb=None):
    """Parse the input stream for VagueTextRecord instances.
    """
    logger.debug('Starting to parse stream verbose=%s', verbose)
    rec = None
    book_row = []
    lines = []
    previous_line = None
    for num, line in enumerate(input):
        line = line.strip()
        if not line:
            if len(lines) > 1:
                try:
                    logger.debug('Yielding VTR')
                    yield VagueTextRecord(book_row + lines)
                except Exception, err:
                    if errorcb: errorcb(num - len(lines), num, book_row + lines, err)
            elif previous_line:
                logger.debug('Changing book_row to "%s"', previous_line)
                book_row = [ previous_line ]
            lines = []
        else:
            logger.debug('Accumultating line %d:"%s"', num, line)
            if verbose:
                print "%05d: %s" % (num+1, line)
            lines.append(line)
        previous_line = line
    if lines:
        try:
            logger.debug('Yielding last VTR')
            yield VagueTextRecord(book_row + lines)
        except Exception, err:
            if errorcb: errorcb(num - len(lines), num, book_row + lines, err)
    return




class ParsedDate(object):
    """Parse a date string and return a datetime.date() instance."""
    # Replace common abbreviations that are incorrect
    def __init__(self, date_string):
        for bad, good in [ ('Sept', 'Sep'), ('July', 'Jul')]:
            date_string = date_string.replace(bad, good)

        if '[sic]' in date_string:
            self.sic = True
            date_string = date_string.replace('[sic]', '').strip()
        else:
            self.sic = False

        parsed_time = time.strptime(date_string, '%d %b %Y')
        self.date = datetime.date(*parsed_time[:3])
        return
    def __str__(self):
        formatted = self.date.strftime('%d %b %Y')
        return formatted
    
class ParsedLine(object):
    """Base class for parsed line types"""
    PATTERN = None
    def __init__(self, line):
        super(ParsedLine, self).__init__()
        if not self.PATTERN:
            raise RuntimeError('No PATTERN specified')
        self.line = line
        if DEBUG:
            print line
            print self.PATTERN.pattern
        match = self.PATTERN.match(line)
        self.data = { }
        self.data.update(match.groupdict())
        if DEBUG:
            print self.data
            print
        for name, value in self.data.items():
            if value is None:
                setattr(self, name, '')
            else:
                setattr(self, name, value)
        return

class ParsedSentence(ParsedLine):
    """Parse a sentence into component parts."""
    PATTERN = re.compile(r"""
        s(?P<kind>[rsc])
        (\s+(?P<amount>\d+(\.\d+)?))?
        (\s+(?P<code>(C|F|J|L|M|O|PD?|R|W)))?
        (\s+(?P<date>\d{1,2}\s+.*?\s+\d{4}))?
        (\s+\((?P<note>.*)\))?
        """,
        re.IGNORECASE | re.VERBOSE)

    KINDS = {
        'R':'Rendered',
        'S':'Served',
        'C':'Contempt',
        }
    CODES = {
        'C':'Cost',
        'F':'Fine',
        'J':'Jail',
        'L':'Labor (days)',
        'M':'Labor (months)',
        'O':'Other',
        'P':'Paid',
        'R':'Remitted by the Mayor',
        'W':'Work',
        None:'Unknown',
        }
    AMT_FMT = {
        'F':'$%0.02f',
        'C':'$%0.02f',
        'P':'$%0.02f',
        }

    def __init__(self, line):
        super(ParsedSentence, self).__init__(line)
        
        self.kind = self.kind.upper()
        self.code = self.code.upper() or 'P'
        if self.code == 'PD':
            self.code = 'P'
        
        if self.amount:
            self.amount = float(self.amount)
        else:
            self.amount = 0.0
            
        if self.date:
            self.date = ParsedDate(self.date)
        else:
            self.date = None
            self.date_sic = False
        return
    def is_served(self):
        return self.kind == 'S'
    def is_rendered(self):
        return self.kind in ('R', 'C')
    def is_dollars(self):
        return self.code in ('F', 'C', 'P')
        
    def __str__(self):        
        kind = self.KINDS[self.kind]
        code = self.CODES[self.code]
        parts = [ '%s: %s' % (kind, code), ]
        parts.append(self.AMT_FMT.get(self.code, '%d') % self.amount)
        if self.date:
            parts.append(str(self.date))
        if self.note:
            parts.append('(%s)' % self.note)
        return ' '.join(parts)
        
class ParsedParticipant(ParsedLine):
    """Participant associated with a case"""
    
    _named_parts = [ 'alias', 'title', 'suffix' ]
    _named_parts_pattern = '|'.join([ r'(\s*%s=(?P<%s>[^\s]+))' % (n, n)
                                      for n in _named_parts 
                                      ])
    
    PATTERN = re.compile(r'''
        (?P<kind>\w+)\s+
        (?P<fullname>([^=]+(\s+|$))+)
        (%s)*
        (\s+\((?P<note>.*)\))?
        ''' % _named_parts_pattern, 
        re.VERBOSE | re.IGNORECASE)
    
    KINDS_TO_ROLES = {
        'AO':'Arresting Officer',
        'D':'Defendant',
        'W':'Witness',
        'WD':'Witness, Defense',
        'DW':'Witness, Defense',
        }
    
    def __init__(self, line):
        super(ParsedParticipant, self).__init__(line)
        self.kind = self.kind.upper()
        self.role = self.KINDS_TO_ROLES[self.kind]
        self.first = ''
        self.middle = ''
        self.last = ''
        if self.fullname:
            self.fullname = string.capwords(self.fullname.strip())
            parts = self.fullname.split() #re.split('\s+', self.fullname.strip())
            self.last = parts[-1]
            parts = parts[:-1]
            if parts:
                self.first = parts[0]
                parts = parts[1:]
                if parts:
                    self.middle = ' '.join(parts)
        self.race = ''
        self.gender = ''
        return
    def __str__(self):
        parts = [ self.role + ':', ]
        if self.first: parts.append(self.first)
        if self.middle: parts.append(self.middle)
        if self.last: parts.append(self.last)
        for name in self._named_parts:
            val = getattr(self, name)
            if val:
                parts.append('%s=%s' % (name, val))
        return ' '.join(parts)


class ParsedViolation(ParsedLine):
    """A violation, either as a code or a note."""
    PATTERN = re.compile(r'''
        v\s+(((?P<code>\d+)(\s*\((?P<note>.*)\))?)|(?P<note2>.*))
        ''',
        re.IGNORECASE | re.VERBOSE)
    def __init__(self, line):
        super(ParsedViolation, self).__init__(line)
        #if not self.code: 
        #    self.code = 'none'
        if self.note2 and not self.note:
            self.note = self.note2
        return
    def __str__(self):
        if self.code and self.note:
            return '%s (%s)' % (self.code, self.note)
        elif self.code:
            return str(self.code)
        else:
            return str(self.note)

class VagueTextRecord(object):
    """Record read from a text source."""
    
    def __init__(self, lines):
        super(VagueTextRecord, self).__init__()
        self.book = None
        self.book_page = None
        self.case = None
        self.arrest_date = None
        self.hearing_date = None
        self.defendant = None
        self.defendant_race = ''
        self.defendant_gender = ''
        self.participants = []
        self.violations = []
        self.sentences = []
        self.note_lines = []
        self.plea = 'U'
        self.outcome = 'U'
        self.location = ''
        
        # Basic parsing
        self.lines = []
        for line in lines:
            line = line.strip()
            if not line:
                continue
            self.lines.append(line)
            code = line[:line.find(' ')].lower()
            try:
                handler = getattr(self, 'feed_%s' % code)
            except AttributeError:
                raise ValueError('Unrecognized code "%s" for "%s"' % (code, line))
            handler(line)
        
        # Validation
        if not self.case:
            raise ValueError('No case')
        if not self.book:
            raise ValueError('No book')
        for p in self.participants:
            if p.role == 'Defendant':
                self.defendant = p
                break
        else:
            raise ValueError('No Defendant found for %s' % self.id())
        
        # Default values for dates
        #if (self.hearing_date is None) and self.sentences:
        #    raise ValueError('There is no hearing date for %s' % self.id())
        for sentence in self.sentences:
             if sentence.date is None and self.hearing_date is not None:
                 sentence.date = self.hearing_date
        
        # Figure out the year for this case
        if self.arrest_date:
            self.year = self.arrest_date.date.year
        else:
            self.year = int(self.book.split('/')[0])
            
        # Copy defendant details
        self.defendant.race = self.defendant_race
        self.defendant.gender = self.defendant_gender
        
        # Merge the notes.
        self.notes = '\n'.join(self.note_lines)
        
        # Look for PD sentences without an amount
        dollars = 0.0
        unpaid = None
        for sentence in self.sentences:
            if sentence.is_served() and sentence.code == 'P' and sentence.amount == 0:
                unpaid = sentence
            elif sentence.is_rendered() and sentence.is_dollars():
                dollars += sentence.amount
        if unpaid is not None:
            unpaid.amount = dollars
            
        # Make sure the page is a number
        if self.book_page is None:
            raise ValueError('No page for %s' % self.id())
        self.book_page = int(self.book_page)
        return
        
    def id(self):
        return 'Case %s in %s' % (self.case, self.book)

    def __str__(self):
        """Return a string representation"""
        lines = [ self.id(),
                  'Page %s' % self.book_page,
                  'Arrest date: %s' % self.arrest_date
                  ]
        
        lines.extend(str(i) for i in self.participants)
        if self.sentences:
            lines.append('Sentences:')
        lines.extend(str(i) for i in self.sentences)
        lines.extend(str(i) for i in self.violations)
        lines.extend(self.note_lines)
        return '\n'.join(lines)
        
    def _get_integer(self, line):
        """the line contains a code and an integer, return the integer"""
        return int(line.split()[-1])
        
    def _get_string(self, line):
        """the line contains a code and a string, return the string"""
        return ' '.join(line.split()[1:])
        
    def _get_date(self, line):
        """The line contains a code and a date, return the date"""
        date_string = self._get_string(line)
        return ParsedDate(date_string)
        
    def _get_participant(self, line):
        """Record the participant with their role"""
        return ParsedParticipant(line)
    def _add_participant(self, line):
        self.participants.append(self._get_participant(line))
        return
    feed_ao = _add_participant
    feed_d = _add_participant
    feed_wd = _add_participant
    feed_dw = _add_participant
    feed_w = _add_participant
    
    def feed_b(self, line):
        """Book"""
        if self.book:
            raise ValueError('Already have a book')
        self.book = self._get_string(line)

    def feed_c(self, line):
        """Case"""
        if self.case:
            raise ValueError('Already have a case number')
        self.case = self._get_string(line)
        
    def feed_pg(self, line):
        """Page number"""
        if self.book_page:
            raise ValueError('Already have a page number')
        self.book_page = self._get_integer(line)
        
    def feed_ad(self, line):
        """Arrest date"""
        if self.arrest_date:
            raise ValueError('Already have an arrest date')
        self.arrest_date = self._get_date(line)

    def feed_hd(self, line):
        """Hearing date"""
        if self.hearing_date:
            raise ValueError('Already have a hearing date')
        self.hearing_date = self._get_date(line)
        
    def feed_l(self, line):
        """Location"""
        self.location = self._get_string(line)
        
    def feed_v(self, line):
        """Violation"""
        self.violations.append(ParsedViolation(line))
            
    def feed_p(self, line):
        """Plea"""
        plea = self._get_string(line).lower()
        plea = {'guilty':'G',
                'guitly':'G', # common typo
                'not guilty':'NG',
                'no context':'NC',
                'unknown':'U',
                }.get(plea, plea.upper())
        if len(plea) > 2:
            raise ValueError('Invalid plea: "%s"' % plea)
        self.plea = plea
        
    def _add_sentence(self, line):
        self.sentences.append(ParsedSentence(line))
    feed_sr = _add_sentence
    feed_ss = _add_sentence
    feed_sc = _add_sentence
        
    def feed_n(self, line):
        """Note"""
        self.note_lines.append(self._get_string(line))
    
    def feed_o(self, line):
        """Outcome"""
        outcome = self._get_string(line).lower().split()[0]
        outcome = { 'guilty':'G',
                    'dismissed':'D',
                    'dismisses':'D', # common typo
                    'not guilty':'NG',
                    'suspended':'S',
                    'unknown':'U',
                   }.get(outcome, outcome.upper())
        self.outcome = outcome
    
    def feed_g(self, line):
        """Gender"""
        if self.defendant_gender:
            raise ValueError('Already have a defendant gender')
        self.defendant_gender = self._get_string(line)[0].upper()

    def feed_r(self, line):
        """Race"""
        if self.defendant_race:
            raise ValueError('Already have a defendant race')
        self.defendant_race = self._get_string(line)[0].upper()
